/**
 * Copyright (C) 2009 Matteo Merli <mm@merlimat.org>
 * 
 * $Id: Context.java 14 2009-01-04 18:01:41Z matteo.merli $
 * $URL: http://pxl-lang.googlecode.com/svn/trunk/src/main/java/pxl/Context.java $
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *         http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package pxl;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import pxl.errors.NameError;
import pxl.types.PxlObject;

/**
 * Context class. A context represents the various stacked scopes.
 * 
 * @author Matteo Merli
 */
public class Context
{

    private static Context instance = new Context();
    
    private Scope currentScope = null;

    private Scope lastScopeSeen = null;

    /**
     * TODO: When using multiple threads, this method should return the
     * per-thread instance, as there will be multiple concurrent contexts.
     * 
     * @return the current Context
     */
    public static Context getInstance()
    {
        return instance;
    }

    private Stack<Scope> scopeStack = new Stack<Scope>();

    /**
     * Private constructor.
     */
    private Context()
    {
        scopeStack.push( GlobalScope.getInstance() );
    }

    /**
     * Add a scope on the top of the stack.
     * 
     * @param scope the new current scope
     */
    public void push( Scope scope )
    {
        currentScope = scope;
        scopeStack.push( scope );
    }

    /**
     * Remove the current scope from the stack.
     * 
     * @return the previous current scope
     * @throws RuntimeException if the stack is empty
     */
    public Scope pop()
    {
        if ( scopeStack.size() == 1 )
        {
            throw new RuntimeException( "Scope stack error" );
        }

        Scope oldScope = scopeStack.pop();
        currentScope = scopeStack.peek();
        return oldScope;
    }

    /**
     * Get the current scope.
     * 
     * @return the current scope
     */
    public Scope getCurrentScope()
    {
        return currentScope;
    }

    /**
     * Object are inserted in the context using the following logic:
     * <ul>
     * <li>If the name it's NOT present in the whole context or it's only
     * present in the global scope, it will be inserted in the current scope.</li>
     * <li>If the name it's present in the current scope, it's value will be
     * replaced.</li>
     * </ul>
     */
    public void put( String name, PxlObject obj )
    {
        getCurrentScope().put( name, obj );
    }

    /**
     * Look for an object in the scope hierarchy
     * 
     * @param name the name of the object to be retrieved from the scope
     * @return the value of the object
     * @throws NameError if the object cannot be found
     */
    public PxlObject get( String name )
    {
        PxlObject obj = null;
        Scope scope = currentScope;
        while ( scope != null )
        {
            obj = scope.get( name );
            if ( obj != null )
                return obj;
            
            scope = scope.getParent();
        }
        
        // Name not found in context
        throw new NameError( name );
    }
    
    public boolean has( String name )
    {
        PxlObject obj = null;
        Scope scope = currentScope;
        while ( scope != null )
        {
            obj = scope.get( name );
            if ( obj != null )
                return true;
            
            scope = scope.getParent();
        }
        
        // Name not found in context
        return false;
    }
    
    /**
     * Updates the value of an object in the nearest scope 
     * 
     * @param name the name of the object to be retrieved from the scope
     * @param newValue the new value to assign to the object
     * @throws NameError if the object cannot be found
     */
    public void update( String name, PxlObject newValue )
    {
        PxlObject obj = null;
        Scope scope = currentScope;
        while ( scope != null )
        {
            obj = scope.get( name );
            if ( obj != null )
            {
                scope.put( name, newValue );
                return;
            }
            
            scope = scope.getParent();
        }
       
        // Name not found in context
        throw new NameError( name );

    }

    public void saveCurrentScope()
    {
        lastScopeSeen = scopeStack.peek();
    }

    public List<Scope> getStackTrace()
    {
        int n = scopeStack.size();

        List<Scope> trace = new ArrayList<Scope>( scopeStack.size() );

        for ( int i = 1; i < n; i++ )
            trace.add( scopeStack.get( i ) );

        if ( lastScopeSeen != null )
        {
            trace.add( lastScopeSeen );
            lastScopeSeen = null;
        }

        return trace;
    }

    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder();
        sb.append( "** CONTEXT  ** \n" );
        Scope scope = currentScope;
        while (scope != GlobalScope.getInstance())
        {
            sb.append( scope ).append( '\n' );
            scope = scope.getParent();
        }
        sb.append( "***************\n" );
        return sb.toString();
    }

}
